#include "SDL.h"
#include "GL/gl3.h"
#include "fmod.hpp"
#include <iostream>
#include <vector>

const GLchar *g_srcVS =
    "#version 420 core\n"
    "in vec4 in_vertex;"
    "void main() {"
        "gl_Position = in_vertex"
    "}";
const GLchar *g_srcFS =
    "#version 420 core\n"
    "out vec4 fs_color;"
    "uniform float u_time;"
    "void main() {"
        "fs_color = vec4(0.5 + 0.5*sin(u_time));"
    "}";

PFNGLCREATESHADERPROGRAMVPROC glCreateShaderProgramv;
PFNGLDELETEPROGRAMPROC glDeleteProgram;
PFNGLPROGRAMUNIFORM1FPROC glProgramUniform1f;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;

PFNGLGENPROGRAMPIPELINESPROC glGenProgramPipelines;
PFNGLDELETEPROGRAMPIPELINESPROC glDeleteProgramPipelines;
PFNGLBINDPROGRAMPIPELINEPROC glBindProgramPipeline;
PFNGLUSEPROGRAMSTAGESPROC glUseProgramStages;

PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
PFNGLDRAWARRAYSPROC glDrawArrays;

PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBUFFERDATAPROC glBufferData;

#if defined(DEBUG)
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
PFNGLGETPROGRAMPIPELINEINFOLOGPROC glGetProgramPipelineInfoLog;
PFNGLVALIDATEPROGRAMPIPELINEPROC glValidateProgramPipeline;
#endif

FMOD::System *g_system;
FMOD::Sound *g_note[12];

GLuint g_VSP; // vertex shader program
GLuint g_FSP; // fragment shader program
GLuint g_pipeline;
GLuint g_vao, g_vbo;

void LoadOpenGLCommands() {
    glCreateShaderProgramv = reinterpret_cast<PFNGLCREATESHADERPROGRAMVPROC>(SDL_GL_GetProcAddress("glCreateShaderProgramv"));
    glDeleteProgram = reinterpret_cast<PFNGLDELETEPROGRAMPROC>(SDL_GL_GetProcAddress("glDeleteProgram"));
    glProgramUniform1f = reinterpret_cast<PFNGLPROGRAMUNIFORM1FPROC>(SDL_GL_GetProcAddress("glProgramUniform1f"));
    glGetUniformLocation = reinterpret_cast<PFNGLGETUNIFORMLOCATIONPROC>(SDL_GL_GetProcAddress("glGetUniformLocation"));

    glGenProgramPipelines = reinterpret_cast<PFNGLGENPROGRAMPIPELINESPROC>(SDL_GL_GetProcAddress("glGenProgramPipelines"));
    glDeleteProgramPipelines = reinterpret_cast<PFNGLDELETEPROGRAMPIPELINESPROC>(SDL_GL_GetProcAddress("glDeleteProgramPipelines"));
    glBindProgramPipeline = reinterpret_cast<PFNGLBINDPROGRAMPIPELINEPROC>(SDL_GL_GetProcAddress("glBindProgramPipeline"));
    glUseProgramStages = reinterpret_cast<PFNGLUSEPROGRAMSTAGESPROC>(SDL_GL_GetProcAddress("glUseProgramStages"));

    glGenVertexArrays = reinterpret_cast<PFNGLGENVERTEXARRAYSPROC>(SDL_GL_GetProcAddress("glGenVertexArrays"));
    glDeleteVertexArrays = reinterpret_cast<PFNGLDELETEVERTEXARRAYSPROC>(SDL_GL_GetProcAddress("glDeleteVertexArrays"));
    glBindVertexArray = reinterpret_cast<PFNGLBINDVERTEXARRAYPROC>(SDL_GL_GetProcAddress("glBindVertexArray"));
    glVertexAttribPointer = reinterpret_cast<PFNGLVERTEXATTRIBPOINTERPROC>(SDL_GL_GetProcAddress("glVertexAttribPointer"));
    glEnableVertexAttribArray = reinterpret_cast<PFNGLENABLEVERTEXATTRIBARRAYPROC>(SDL_GL_GetProcAddress("glEnableVertexAttribArray"));
    glDrawArrays = reinterpret_cast<PFNGLDRAWARRAYSPROC>(SDL_GL_GetProcAddress("glDrawArrays"));

    glGenBuffers = reinterpret_cast<PFNGLGENBUFFERSPROC>(SDL_GL_GetProcAddress("glGenBuffers"));
    glDeleteBuffers = reinterpret_cast<PFNGLDELETEBUFFERSPROC>(SDL_GL_GetProcAddress("glDeleteBuffers"));
    glBindBuffer = reinterpret_cast<PFNGLBINDBUFFERPROC>(SDL_GL_GetProcAddress("glBindBuffer"));
    glBufferData = reinterpret_cast<PFNGLBUFFERDATAPROC>(SDL_GL_GetProcAddress("glBufferData"));

#if defined(DEBUG)
    glGetProgramInfoLog = reinterpret_cast<PFNGLGETPROGRAMINFOLOGPROC>(SDL_GL_GetProcAddress("glGetProgramInfoLog"));
    glGetProgramPipelineInfoLog = reinterpret_cast<PFNGLGETPROGRAMPIPELINEINFOLOGPROC>(SDL_GL_GetProcAddress("glGetProgramPipelineInfoLog"));
    glValidateProgramPipeline = reinterpret_cast<PFNGLVALIDATEPROGRAMPIPELINEPROC>(SDL_GL_GetProcAddress("glValidateProgramPipeline"));
#endif
}

int main() {
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
    SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
    SDL_WindowID win = SDL_CreateWindow("test4", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS);
    SDL_GL_CreateContext(win);

    LoadOpenGLCommands();

    // vertex shader program
    g_VSP = glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &g_srcVS);
#if defined(DEBUG)
    GLchar log[1024];
    GLsizei logLen;
    glGetProgramInfoLog(g_VSP, sizeof(log), &logLen, log);
    if (logLen) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Vertex shader program info log:\n%s", log);
    }
#endif

    // fragment shader program
    g_FSP = glCreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &g_srcFS);
#if defined(DEBUG)
    glGetProgramInfoLog(g_FSP, sizeof(log), &logLen, log);
    if (logLen) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Fragment shader program info log:\n%s", log);
    }
#endif

    glGenProgramPipelines(1, &g_pipeline);
    glUseProgramStages(g_pipeline, GL_VERTEX_SHADER_BIT, g_VSP);
    glUseProgramStages(g_pipeline, GL_FRAGMENT_SHADER_BIT, g_FSP);
#if defined(DEBUG)
    glValidateProgramPipeline(g_pipeline);
    glGetProgramPipelineInfoLog(g_pipeline, sizeof(log), &logLen, log);
    if (logLen) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Program pipeline info log:\n%s", log);
    }
#endif

    glGenVertexArrays(1, &g_vao);
    glBindVertexArray(g_vao);

    const float bufferData[] = { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f };
    glGenBuffers(1, &g_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(bufferData), bufferData, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    FMOD::System_Create(&g_system);
    g_system->init(100, FMOD_INIT_NORMAL, NULL);

    for (int n = 0; n < 12; ++n) {
        const float freq = 440.0f*pow(2.0f,(n-21)/12.0f);

        std::vector<float> data(44100);
        for (std::size_t i = 0; i < data.size(); ++i) {
            const float t = i/44100.0f;
            const float w = 2.0f*3.1415926f*freq;

            float y = 0.6f*sin(1.0f*w*t)*exp(-0.0008f*w*t);
            y += 0.3f*sin(2.0f*w*t)*exp(-0.0010f*w*t);
            y += 0.1f*sin(4.0f*w*t)*exp(-0.0015f*w*t);
            y += 0.2f*y*y*y;
            y *= 0.9f + 0.1*cos(70.0f*t);
            y = 2.0f*y*exp(-22.0f*t) + y;

            data[i] = y;
        }

        FMOD_CREATESOUNDEXINFO info = { 0 };
        info.cbsize = sizeof(info);
        info.length = data.size() * sizeof(float);
        info.defaultfrequency = 44100;
        info.numchannels = 1;
        info.format = FMOD_SOUND_FORMAT_PCMFLOAT;
        g_system->createSound(reinterpret_cast<const char *>(&data[0]), FMOD_DEFAULT | FMOD_OPENRAW | FMOD_OPENMEMORY, &info, &g_note[n]);
    }

    bool run = true;
    while (run) {
        SDL_Event evt;
        while (SDL_PollEvent(&evt)) {
            if (evt.type == SDL_KEYDOWN) {
                switch (evt.key.keysym.sym) {
                case SDLK_ESCAPE:
                    run = false;
                    break;
                case SDLK_z: g_system->playSound(FMOD_CHANNEL_FREE, g_note[0], false, NULL); break;
                case SDLK_x: g_system->playSound(FMOD_CHANNEL_FREE, g_note[1], false, NULL); break;
                case SDLK_c: g_system->playSound(FMOD_CHANNEL_FREE, g_note[2], false, NULL); break;
                case SDLK_v: g_system->playSound(FMOD_CHANNEL_FREE, g_note[3], false, NULL); break;
                case SDLK_b: g_system->playSound(FMOD_CHANNEL_FREE, g_note[4], false, NULL); break;
                case SDLK_n: g_system->playSound(FMOD_CHANNEL_FREE, g_note[5], false, NULL); break;
                case SDLK_m: g_system->playSound(FMOD_CHANNEL_FREE, g_note[6], false, NULL); break;
                }
            }
        }

        g_system->update();

        glProgramUniform1f(g_FSP, glGetUniformLocation(g_FSP, "u_time"), 0.001f*SDL_GetTicks());
        glBindProgramPipeline(g_pipeline);
        glBindVertexArray(g_vao);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        SDL_GL_SwapWindow(win);
    }

    glDeleteProgram(g_VSP);
    glDeleteProgram(g_FSP);
    glDeleteProgramPipelines(1, &g_pipeline);
    glDeleteVertexArrays(1, &g_vao);
    glDeleteBuffers(1, &g_vbo);

    for (int i = 0; i < 12; ++i) {
        g_note[i]->release();
    }
    g_system->release();

    SDL_Quit();

    return 0;
}
